/*
 * ATNF Javascript library
 * Copyright (C) 2012 Jamie Stevens
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/**
 * @fileoverview This library contains useful objects for interacting
 * with the ATNF telescopes (usually through MoniCA), and more general
 * astronomical tasks such as coordinate conversion and handling of time.
 * @author ste616@gmail.com (Jamie Stevens)
 */

// Our Dojo requirements are listed here.
dojo.require('dojo.date');
dojo.require('dojo.number');
dojo.require('dojox.timing._base');

/**
 * This function is the basis for automated monitoring of data points
 * relating to a particular telescope component, although the component
 * can be arbitrarily defined. The user asks this object to monitor a
 * set of MoniCA points, and monitor them regularly for changes, and/or
 * to trigger events if the points meet user-defined criteria.
 * @param {object} spec Specifications for the object setup.
 * @param {object} my   An object reference that this object should be able
 *                      to access.
 */
var telescopeComponent = function(spec, my) {
	/**
	 * The object we return to our caller.
	 * @type {object}
	 */
	var that = {};
	
	// Set some sensible defaults in the spec object.
	spec = spec || {};
	
	/**
	 * The MoniCA handler object.
	 * @type {object}
	 */
	spec.monica = spec.monica || null;
	
	/**
	 * The points related to this telescope component.
	 * @type {array}
	 */
	var relatedPoints = [];
	
	/**
	 * A list of error conditions that we should check for.
	 * @type {array}
	 */
	var errorConditions = [];
	
	/**
	 * The condition we use to decide when to evaluate this component's status.
	 * @type {object}
	 */
	var checkCondition = {};
	
	/**
	 * The function to call every time the component is evaluated.
	 * @type {function}
	 */
	var checkCall = null;
	
	/**
	 * The function to call when the component is evaluated to be in error.
	 * @type {function}
	 */
	var errorCall = null;

	/**
	 * A timer required for periodic evaluation.
	 * @type {Timer}
	 */
	var evaluateTimer = new dojox.timing.Timer();

	/**
	 * The known types of error checks.
	 * @type {array}
	 */
	var knownErrorActions = [ 'valueAbove', 'valueBelow',
														'valueRange', 'errorState' ];

	/**
	 * The usable default error check routines.
	 * @type {array}
	 */
	var usableErrorRoutines = {};
	
	// Our methods follow.

	// Our private methods.
	/**
	 * Check that we have a MoniCA connection.
	 */
	var hasMoniCA = function() {
		if (spec.monica !== null) {
			return true;
		} else {
			return false;
		}
	};

	/**
	 * Set a default logic if required for the error checkers.
	 * @param {array} cArray The conditions to check.
	 * @param {string} logic The way to handle multiple checks.
	 */
	var defaultLogic = function(cArray, logic) {
		if (cArray.length === 1) {
			// Only a single point.
			logic = 'or';
		} else {
			// Set a default for the logic.
			logic = logic || 'and';
		}
		
		return logic;
	};
	
	/**
	 * Evaluate a logic combination.
	 * @param {array} conditions A set of Booleans indicating the state of
	 *                           several conditions.
	 * @param {string} logic The way to handle the Boolean combination.
	 */
	var evaluateLogic = function(conditions, logic) {
		var endCondition = conditions[0];
		for (var l = 1; l < conditions.length; l++) {
			if (logic === 'and') {
				endCondition = endCondition && conditions[l];
			} else if (logic === 'or') {
				endCondition = endCondition || conditions[l];
			}
		}
		
		return endCondition;
	};
	
	/**
	 * Error checking method: check if a number of point values
	 * are larger than some reference values.
	 * @param {array} pointNames The names of the points to check.
	 * @param {array} refValues The comparison values.
	 * @param {string} logic The way to handle multiple checks.
	 */
	usableErrorRoutines.valueAbove = function(pointNames, refValues, logic) {
		logic = defaultLogic(pointNames, logic);
		
		// Go through the points.
		var conditions = [];
		for (var p = 0; p < pointNames.length; p++) {
			// Get the latest value for this point.
			var pVal = findPointReference(pointNames[p]).latestValue().value;
			if (pVal > refValues[p]) {
				conditions[p] = true;
			} else {
				conditions[p] = false;
			}
		}

		return evaluateLogic(conditions, logic);
	};

	/**
	 * Error checking method: check if a number of point values
	 * are smaller than some reference values.
	 * @param {array} pointNames The names of the points to check.
	 * @param {array} refValues The comparison values.
	 * @param {string} logic The way to handle multiple checks.
	 */
	usableErrorRoutines.valueBelow = function(pointNames, refValues, logic) {
		logic = defaultLogic(pointNames, logic);
		
		// Go through the points.
		var conditions = [];
		for (var p = 0; p < pointNames.length; p++) {
			// Get the latest value for this point.
			var pVal = findPointReference(pointNames[p]).latestValue().value;
			if (pVal < refValues[p]) {
				conditions[p] = true;
			} else {
				conditions[p] = false;
			}
		}

		return evaluateLogic(conditions, logic);
	};

	/**
	 * Error checking method: check if a number of point values
	 * are within than some reference ranges.
	 * @param {array} pointNames The names of the points to check.
	 * @param {array} refValues The comparison values.
	 * @param {string} logic The way to handle multiple checks.
	 */
	usableErrorRoutines.valueRange = function(pointNames, refValues, logic) {
		logic = defaultLogic(pointNames, logic);
		
		// Go through the points.
		var conditions = [];
		for (var p = 0; p < pointNames.length; p++) {
			// Get the latest value for this point.
			var pVal = findPointReference(pointNames[p]).latestValue().value;
			if (pVal >= refValues[p][0] &&
					pVal <= refValues[p][1]) {
				conditions[p] = true;
			} else {
				conditions[p] = false;
			}
		}

		return evaluateLogic(conditions, logic);
	};

	/**
	 * Error checking method: check if a number of point error states
	 * are in some reference states.
	 * @param {array} pointNames The names of the points to check.
	 * @param {array} refValues The comparison states.
	 * @param {string} logic The way to handle multiple checks.
	 */
	usableErrorRoutines.errorState = function(pointNames, refValues, logic) {
		logic = defaultLogic(pointNames, logic);
		
		// Go through the points.
		var conditions = [];
		for (var p = 0; p < pointNames.length; p++) {
			// Get the latest error state for this point.
			var pVal = findPointReference(pointNames[p]).latestValue().errorState;
			if (pVal == refValues[p]) {
				conditions[p] = true;
			} else {
				conditions[p] = false;
			}
		}

		return evaluateLogic(conditions, logic);
	};

	
	/**
	 * Start monitoring a new point.
	 * @param {string} pointName The name of the MoniCA point to add.
	 */
	var addPoint = function(pointName) {
		// Check that we have a MoniCA connection.
		if (hasMoniCA() === false) {
			return;
		}
		
		// Check that the point name exists on the MoniCA server.
		if (spec.monica.isPoint(pointName) === false) {
			return;
		}
		
		// Add the point and get its reference.
		var nPointObject = {
			name: pointName
		};
		var pRef = spec.monica.addPoints([ pointName ]);
		nPointObject.reference = pRef[0];

		// Direct the server to call our callback function when the point updates.
		nPointObject.reference.addCallback(pointUpdated);
		
		// Add it to our list.
		relatedPoints.push(nPointObject);
	};

	/**
	 * This routine gets called when a point we handle is updated.
	 * @param {object} pointRef The point that has been updated.
	 */
	var pointUpdated = function(pointRef) {
		// Which point has been updated?
		var pName = pointRef.getPointDetails().name;
		
		// Do we evaluate on this point?
		if (typeof checkCondition.point !== 'undefined' &&
				(checkCondition.point === '*' ||
				 checkCondition.point === pName)) {
			// Call our evaluator.
			evaluateCondition();
		}
		if (typeof checkCondition.points !== 'undefined' &&
				dojo.isArray(checkCondition.points)) {
			for (var i = 0; i < checkCondition.points.length; i++) {
				if (checkCondition.points[i] === pName) {
					// Call our evaluator.
					evaluateCondition();
				}
			}
		}
	};

	/**
	 * Determine whether an error condition is met or not.
	 * @param {object} eCond The error condition to evaluate.
	 */
	var evaluateErrorCondition = function(eCond) {
		var isError = false;
		eCond.logic = defaultLogic(eCond.conditions, eCond.logic);
		
		// Use the action functions to determine the result.
		var condRes = [];
		for (var cc = 0; cc < eCond.conditions.length; cc++) {
			condRes[cc] =
				eCond.conditions[cc].action(eCond.conditions[cc].pointNames,
																		eCond.conditions[cc].values);
			// Make some object parameters to help the user make their
			// messages.
			eCond.conditions[cc].pointDetails = [];
			eCond.conditions[cc].pointValues = [];
			for (var pc = 0; pc < eCond.conditions[cc].pointNames.length; pc++) {
				eCond.conditions[cc].pointDetails[pc] =
					findPointReference(eCond.conditions[cc].pointNames[pc]).getPointDetails();
				eCond.conditions[cc].pointValues[pc] =
					findPointReference(eCond.conditions[cc].pointNames[pc]).latestValue();
			}
		}
		
		isError = evaluateLogic(condRes, eCond.logic);
		
		// Set a triggered flag.
		eCond.triggered = isError;

		
		// If it is in error, generate any user-specified messages.
		if (isError === true &&
				dojo.isFunction(eCond.msgFn) === true) {
			eCond.message = eCond.msgFn(eCond.conditions);
		}
		
		return isError;
	};
	
	/**
	 * Find the point reference associated with a named point.
	 * @param {string} pName The name of the point to search for.
	 */
	var findPointReference = function(pName) {
		for (var k = 0; k < relatedPoints.length; k++) {
			if (relatedPoints[k].name === pName) {
				return relatedPoints[k].reference;
			}
		}
		
		return null;
	};
		
	/**
	 * This routine gets called when we are told to evaluate our status.
	 */
	var evaluateCondition = function() {
		// If we've been asked to call a function every time, we do that now.
		checkCall(that);
		
		var inError = false;

		// Go through each of the error conditions we have and evaluate them.
		for (var i = 0; i < errorConditions.length; i++) {
			inError = inError || 
				evaluateErrorCondition(errorConditions[i]);
		}
		
		// If we've determined that we have an error condition, we call the
		// specified error callback.
		if (inError === true) {
			errorCall(that);
		}
	};

	/**
	 * Interpret our conditions object, so we can start a timer if necessary.
	 */
	var interpretCheckConditions = function() {
		// Stop our current timer.
		evaluateTimer.stop();
		
		// Check if we have a time component in the options.
		if (typeof checkCondition.time !== 'undefined') {
			evaluateTimer.setInterval(checkCondition.time * 1000);
			evaluateTimer.onTick = evaluateCondition;
			evaluateTimer.start();
		}
		
		// Do we set a calling function?
		if (typeof checkCondition.callback !== 'undefined' &&
				dojo.isFunction(checkCondition.callback)) {
			checkCall = checkCondition.callback;
			errorCall = checkCondition.callback;
		}
		if (typeof checkCondition.errorCallback !== 'undefined' &&
				dojo.isFunction(checkCondition.errorCallback)) {
			errorCall = checkCondition.errorCallback;
		}
	};

	/**
	 * Add a new check to the error conditions.
	 * @param {object} condition The specification of the error condition.
	 */
	var addErrorCheck = function(condition) {
		// Check for an array of conditions.
		if (typeof condition.conditions === 'undefined' ||
				dojo.isArray(condition.conditions) === false) {
			return;
		}

		// Go through the conditions one at a time.
		var allActionsKnown = true;
		for (var cc = 0; cc < condition.conditions.length; cc++) {
			// Check for point names.
			var allPointsKnown = true;
			if (dojo.isArray(condition.conditions[cc].pointNames) === false) {
				allPointsKnown = false;
			} else {
				for (var cp = 0; cp < condition.conditions[cc].pointNames.length;
						 cp++) {
					allPointsKnown = allPointsKnown &&
						checkForPoint(condition.conditions[cc].pointNames[cp]);
				}
			}

			// Check for a known action.
			var actionKnown = false;
			for (var ca = 0; ca < knownErrorActions.length; ca++) {
				if (condition.conditions[cc].action ===
						knownErrorActions[ca]) {
					actionKnown = true;
					// Replace this string with the appropriate function.
					condition.conditions[cc].action =
						usableErrorRoutines[knownErrorActions[ca]];
				} else if (dojo.isFunction(condition.conditions[cc].action)) {
					actionKnown = true;
				}
			}

			// Check for the right number of values.
			var allValuesPresent = true;
			if (dojo.isArray(condition.conditions[cc].values) === false) {
				allValuesPresent = false;
			} else if (condition.conditions[cc].pointNames.length !== 
								 condition.conditions[cc].values.length) {
				allValuesPresent = false;
			}
			
			// Add it all up.
			allActionsKnown = allActionsKnown && allPointsKnown &&
				allValuesPresent;
		}
		
		if (allActionsKnown === false) {
			return;
		}
		
		// All the checks have been satisfied, add it to the list.
		errorConditions.push(condition);
	};
	
	/**
	 * Check for a named point.
	 * @param {string} pName The name of the point to check for.
	 */
	var checkForPoint = function(pName) {
		for (var j = 0; j < relatedPoints.length; j++) {
			if (pName === relatedPoints[j].name) {
				return true;
			}
		}
		
		return false;
	};
	
	/**
	 * Check for a known error action.
	 * @param {object} eCond The object to check for a known error action.
	 */
	var checkForAction = function(eCond) {
		for (var j = 0; j < knownErrorActions.length; j++) {
			if (typeof eCond[knownErrorActions[j]] !== 'undefined') {
				return true;
			}
		}
		
		return false;
	};

	/**
	 * Find all the error conditions referring to a specified point name.
	 * @param {string} pointName The name of the point to search for.
	 */
	var errorConditionSearch = function(pointName) {
		var foundConditions = [];
		
		for (var i = 0; i < errorConditions.length; i++) {
			if (dojo.isArray(errorConditions[i].pointNames) === true) {
				for (var j = 0; j < errorConditions[i].pointNames.length; j++) {
					if (errorConditions[i].pointNames[j] === pointName) {
						foundConditions.push(errorConditions[i]);
						break;
					}
				}
			} else {
				if (errorConditions[i].pointNames === pointName) {
					foundConditions.push(errorConditions[i]);
				}
			}
		}
		
		return foundConditions;
	};

	// Our public methods.
	/**
	 * Start monitoring new point(s).
	 * @param {array} nPoints A single new point, or an array of points.
	 */
	that.addPoints = function(nPoints) {
		if (dojo.isArray(nPoints)) {
			// We have more than one point.
			for (var k = 0; k < nPoints.length; k++) {
				if (dojo.isString(nPoints[k])) {
					addPoint(nPoints[k]);
				}
			}
		} else if (dojo.isString(nPoints)) {
			addPoint(nPoints);
		}
		
		// Get the point descriptions.
		spec.monica.getDescriptions();
	};

	/**
	 * Set the conditions we will use to determine when we should
	 * evaluate our state.
	 * @param {object} evaluationOptions The conditions object.
	 */
	that.setCheckConditions = function(evaluationOptions) {
		// As long as the passed argument is an object, we simply
		// copy it over to the checkCondition variable.
		if (evaluationOptions !== null &&
				dojo.isFunction(evaluationOptions) === false &&
				dojo.isArray(evaluationOptions) === false &&
				dojo.isObject(evaluationOptions) === true) {
			checkCondition = evaluationOptions;
		}
		
		// Interpret what we received.
		interpretCheckConditions();
	};

	/**
	 * Add some error conditions.
	 * @param {array} nConditions A single new condition, or an array
	 *                            of conditions.
	 */
	that.addErrorConditions = function(nConditions) {
		if (dojo.isArray(nConditions)) {
			// We have more than one condition.
			for (var k = 0; k < nConditions.length; k++) {
				if (dojo.isObject(nConditions[k])) {
					addErrorCheck(nConditions[k]);
				}
			}
		} else if (dojo.isObject(nConditions)) {
			addErrorCheck(nConditions);
		}
	};

	/**
	 * Return the MoniCA references for our points.
	 */
	that.getPointInformation = function() {
		return relatedPoints;
	};

	/**
	 * Return the MoniCA reference for a single point.
	 * @param {string} pointName The name of the point to find and return.
	 */
	that.getSinglePoint = function(pointName) {
		return findPointReference(pointName);
	};

	/**
	 * Return all the objects that are triggering an error condition.
	 */
	that.getErrorConditions = function() {
		var errorConditionsNow = [];
		for (var ec = 0; ec < errorConditions.length; ec++) {
			if (errorConditions[ec].triggered === true) {
				errorConditionsNow.push(errorConditions[ec]);
			}
		}
		
		return errorConditionsNow;
	};
	
	return that;
};

/**
 * This function allows the user to compute different astronomically
 * useful time quantities, such as sidereal time and MJD. It also makes it
 * easy to switch time zones reliably.
 * @param {object} spec Specifications for the object setup.
 * @param {object} my   An object reference that this object should be able
 *                      to access.
 */
var astroTime = function(spec, my) {
	
	/**
	 * The object we return to our caller.
	 * @type {object}
	 */
	var that = {};

	// Set some useful defaults in the spec object.
	spec = spec || {};
	
	/**
	 * Our time, which by default is set now.
	 * @type {Date}
	 */
	var time = new Date();

	/**
	 * Our location.
	 * @type {object}
	 */
	var location;
	
	/**
	 * Some locations that we know about.
	 * @type {array}
	 */
	var locations = [
		{
			telescope: 'ATCA',
			longitude: 149.550138889,
			latitude: -30.312884722,
			timezoneOffset: 600
		}
	];

	// Our methods follow.
	
	// Our private methods.
	/**
	 * Initialise our instance.
	 */
	var initialise = function() {
		// Check for our constructor options.
		if (spec.utcString) {
			// The user has supplied us with a string representation of
			// the time we should be set to.
			var calTime = string2cal(spec.utcString);
			cal2time(calTime);
		} else if (spec.date) {
			// The user has supplied a Date object to match.
			time.setTime(spec.date.getTime());
		}
		
		// Do we have a specified location?
		if (spec.location &&
				typeof spec.location.telescope !== 'undefined' &&
				typeof spec.location.longitude !== 'undefined' &&
				typeof spec.location.latitude !== 'undefined' &&
				typeof spec.location.timezoneOffset !== 'undefined') {
			location = spec.location;
		} else {
			// By default we are located at ATCA.
			setLocation('ATCA');
		}
	};

	/**
	 * Set the location based on the name of the telescope.
	 * @param {string} telescope The name of the telescope.
	 */
	var setLocation = function(telescope) {
		for (var i = 0; i < locations.length; i++) {
			if (locations[i].telescope === telescope) {
				location = locations[i];
			}
		}
	};
	
	/**
	 * Turn a string representation of the time into a calendar object.
	 * @param {string} stringTime The string representation of the time.
	 */
	var string2cal = function(stringTime) {
		if (!dojo.isString(stringTime)) {
			return null;
		}
		
		var calendarObj = {};
		
		var tElements = stringTime.split(/[\s\-\:T]/g);
		if (tElements[0]) {
			calendarObj.year = tElements[0];
		} else {
			return null;
		}
		if (tElements[1]) {
			calendarObj.month = tElements[1];
		} else {
			return null;
		}
		if (tElements[2]) {
			calendarObj.day = tElements[2];
		} else {
			return null;
		}
		if (tElements[3]) {
			calendarObj.hour = tElements[3];
		} else {
			return null;
		}
		if (tElements[4]) {
			calendarObj.minute = tElements[4];
		} else {
			return null;
		}
		if (tElements[5]) {
			calendarObj.second = tElements[5];
		} else {
			return null;
		}
		
		return calendarObj;
	};
	
	/**
	 * Set our object time by a UTC calendar object.
	 * @param {object} calendarObj The calendar object specifying the time.
	 */
	var cal2time = function(calendarObj) {
		if (!calendarObj) {
			return;
		}
		
		if (typeof calendarObj.year !== 'undefined' &&
				typeof calendarObj.month !== 'undefined' &&
				typeof calendarObj.day !== 'undefined' &&
				typeof calendarObj.hour !== 'undefined' &&
				typeof calendarObj.minute !== 'undefined' &&
				typeof calendarObj.second !== 'undefined') {
			time.setUTCFullYear(calendarObj.year);
			time.setUTCMonth(calendarObj.month - 1);
			time.setUTCDate(calendarObj.day);
			time.setUTCHours(calendarObj.hour);
			time.setUTCMinutes(calendarObj.minute);
			time.setUTCSeconds(calendarObj.second);
			time.setUTCMilliseconds(0);
		}
	};

	/**
	 * Turn a calendar object into an MJD.
	 * @param {object} calendarObj The calendar object.
	 */
	var cal2mjd = function(calendarObj) {
		var dayFraction = getDayFraction(calendarObj);
		var m, y;
		if (calendarObj.month <= 2) {
			m = calendarObj.month + 9;
			y = calendarObj.year - 1;
		} else {
			m = calendarObj.month - 3;
			y = calendarObj.year;
		}
		
		var c = parseInt(y / 100);
		y -= c * 100;
		var x1 = parseInt(146097.0 * c / 4.0);
		var x2 = parseInt(1461.0 * y / 4.0);
		var x3 = parseInt((153.0 * m + 2.0) / 5.0);
		
		var mjd = x1 + x2 + x3 + calendarObj.day - 678882 + dayFraction;
		
		return mjd;
	};
	
	/**
	 * Calculate the fraction of the day from a calendar object.
	 * @param {object} calendarObj The calendar object.
	 */
	var getDayFraction = function(calendarObj) {
		var dayFrac = (calendarObj.hour + calendarObj.minute / 60 +
											 calendarObj.second / 3600) / 24;
		
		return dayFrac;
	};

	/**
	 * Turn a day fraction into a partial calendar object.
	 * @param {Number} dayFrac The day fraction.
	 */
	var dayFraction2cal = function(dayFrac) {
		var calendarObj = {};
		var tmp = dayFrac * 24.0;
		calendarObj.hour = Math.floor(tmp);
		tmp -= calendarObj.hour;
		tmp *= 60.0;
		calendarObj.minute = Math.floor(tmp);
		tmp -= calendarObj.minute;
		tmp *= 60.0;
		calendarObj.second = Math.floor(tmp);
		
		return calendarObj;
	};

	/**
	 * Turn any time into a localtime calendar object.
	 * @param {Date} lTime The time to convert.
	 */
	var anytime2localCal = function(lTime) {
		var calendarObj = {};
		
		calendarObj.year = lTime.getFullYear();
		calendarObj.month = lTime.getMonth() + 1;
		calendarObj.day = lTime.getDate();
		calendarObj.hour = lTime.getHours();
		calendarObj.minute = lTime.getMinutes();
		calendarObj.second = lTime.getSeconds();
		
		return calendarObj;
	};

	/**
	 * Turn any time into a UTC calendar object.
	 * @param {Date} uTime The time to convert.
	 */
	var anytime2utcCal = function(uTime) {
		var calendarObj = {};
		
		calendarObj.year = uTime.getUTCFullYear();
		calendarObj.month = uTime.getUTCMonth() + 1;
		calendarObj.day = uTime.getUTCDate();
		calendarObj.hour = uTime.getUTCHours();
		calendarObj.minute = uTime.getUTCMinutes();
		calendarObj.second = uTime.getUTCSeconds();
		
		return calendarObj;
	};
	
	/**
	 * Turn our time into a calendar object.
	 */
	var time2cal = function() {
		return anytime2utcCal(time);
	};

	/**
	 * Turn an MJD into the GMST.
	 * @param {Number} mjd The MJD.
	 */
	var mjd2gmst = function(mjd) {
		/**
		 * The Julian date at the start of the epoch.
		 */
		var jdJ2000 = 2451545.0;
		/**
		 * The number of days in a century.
		 */
		var jdCentury = 36525.0;
		/**
		 * Conversion factor between solar and sidereal time.
		 */
		var solar2sidereal = 1.002737909350795;
		
		var dUT1 = 0.0;
		if (typeof location.dUT1 !== 'undefined') {
			dUT1 = location.dUT1;
		}
		
		var a = 101.0 + 24110.54841/86400.0;
		var b = 8640184.812866/86400.0;
		var e = 0.093104/86400.0;
		var d = 0.0000062/86400.0;
		
		var tu = (Math.floor(mjd) - (jdJ2000 - 2400000.5)) / jdCentury;
		var sidTim = a + tu * (b + tu * (e - tu * d));
		sidTim -= Math.floor(sidTim);
		if (sidTim < 0.0) {
			sidTim += 1.0;
		}
		
		var gmst = sidTim + (mjd - Math.floor(mjd) + dUT1 / 86400.0) *
			solar2sidereal;
		while(gmst < 0.0) {
			gmst += 1.0;
		}
		while (gmst > 1.0) {
			gmst -= 1.0;
		}

		return gmst;
	};

	/**
	 * Turn a GST into the LMST, according to our location.
	 * @param {Number} gmst The GMST.
	 */
	var gmst2lmst = function(gmst) {
		var lst = gmst + deg2turn(location.longitude);
		while (lst > 1.0) {
			lst -= 1.0;
		}
		while (lst < 0.0) {
			lst += 1.0;
		}
		var lmst = lst;

		return lmst;
	};
	
	/**
	 * Turn an angle into the corresponding number of turns.
	 * @param {Number} angle The angle.
	 */
	var deg2turn = function(angle) {
		return (angle / 360.0);
	};

	/**
	 * Turn a calendar object into a string.
	 * @param {object} calendarObj The calendar object.
	 * @param {string} timeFormat An optional format specification.
	 */
	var cal2string = function(calendarObj, timeFormat) {
		timeFormat = timeFormat || '%y-%m-%d %H:%M:%S';

		// Some days.
		var dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
		var monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
											'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
		
		var timeString = timeFormat;
		var r;
		// Go through our supported specifiers and replace them with the
		// proper quantities.
		if (/\%y/.test(timeString) === true) {
			// The year.
			r = calendarObj.year;
			timeString = timeString.replace(/\%y/, '' + r);
		} 
		if (/\%m/.test(timeString) === true) {
			// The month.
			r = zeroPad(calendarObj.month);
			timeString = timeString.replace(/\%m/, r);
		}
		if (/\%d/.test(timeString) === true) {
			// The date.
			r = zeroPad(calendarObj.day);
			timeString = timeString.replace(/\%d/, r);
		}
		if (/\%H/.test(timeString) === true) {
			// The hour.
			r = zeroPad(calendarObj.hour);
			timeString = timeString.replace(/\%H/, r);
		}
		if (/\%M/.test(timeString) === true) {
			// The minute.
			r = zeroPad(calendarObj.minute);
			timeString = timeString.replace(/\%M/, r);
		}
		if (/\%S/.test(timeString) === true) {
			// The second.
			r = zeroPad(calendarObj.second);
			timeString = timeString.replace(/\%S/, r);
		}
		if (/\%O/.test(timeString) === true) {
			// The month name.
			r = monthNames[calendarObj.month - 1];
			timeString = timeString.replace(/\%O/, r);
		}
		
		return timeString;
	};

	/**
	 * Make a number into a string, padding it with a zero if it's
	 * smaller than 10.
	 * @param {Number} n The number.
	 */
	var zeroPad = function(n) {
		var N;
		if (n < 10) {
			N = '0' + n;
		} else {
			N = '' + n;
		}
		
		return N;
	};

	/**
	 * Return the difference between a specified offset and the
	 * timezone on this computer.
	 * @param {number} offsetMinutes An offset in minutes.
	 */
	var tzDifference = function(offsetMinutes) {
		// Get the current timezone offset for our dates.
		var cOffset = -1 * time.getTimezoneOffset();
		// Now subtract this from the requested offset.
		var rOffset = offsetMinutes - cOffset;

		return rOffset;
	};
	
	// Our public methods.
	/**
	 * Return our MJD.
	 */
	that.mjd = function() {
		var calObj = time2cal();
		var mjd = cal2mjd(calObj);
		
		return mjd;
	};

	/**
	 * Return our time in string form.
	 * @param {string} timeFormat An optional time format specification.
	 */
	that.timeString = function(timeFormat) {
		var calObj = time2cal();
		var ts = cal2string(calObj, timeFormat);
		
		return ts;
	};

	/**
	 * Return our time in Date object form.
	 */
	that.time = function() {
		return time;
	};

	/**
	 * Return the local time at the telescope in string form.
	 * @param {string} timeFormat An optional time format specification.
	 */
	that.localtimeString = function(timeFormat) {
		var rDate = dojo.date.add(time, 'minute',
			tzDifference(location.timezoneOffset));
		var calObj = anytime2localCal(rDate);
		var ts = cal2string(calObj, timeFormat);
		
		return ts;
	};
	
	/**
	 * Return our LMST as a fraction of a day.
	 */
	that.lmst = function() {
		var calObj = time2cal();
		var mjd = cal2mjd(calObj);
		var gmst = mjd2gmst(mjd);
		var lmst = gmst2lmst(gmst);
		
		return lmst;
	};
	
	/**
	 * Return our LMST as a string.
	 * @param {string} timeFormat An optional time format specification.
	 */
	that.lmstString = function(timeFormat) {
		timeFormat = timeFormat || '%H:%M:%S';
		var calObj = time2cal();
		var mjd = cal2mjd(calObj);
		var gmst = mjd2gmst(mjd);
		var lmst = gmst2lmst(gmst);
		var lmstCal = dayFraction2cal(lmst);
		var ts = cal2string(lmstCal, timeFormat);
		
		return ts;
	};
	
	/**
	 * Update our time to right now.
	 */
	that.now = function() {
		time = new Date();
		return that;
	};

	// Perform the initialisation.
	initialise();
	
	return that;
};

/**
 * This function allows the user to specify an astronomical source and then
 * get useful information about it, such as its rise and set times, its
 * coordinates in other systems, etc.
 * @param {object} spec Specifications for the object setup.
 * @param {object} my   An object reference that this object should be able
 *                      to access.
 */
var astroSource = function(spec, my) {
	
};

